﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using XcoAppSpaces.Contracts.Exceptions;
using XcoAppSpaces.Contracts.Transport;
using System.Threading;
using System.Linq;
using XcoAppSpaces.Contracts.Logging;
using XcoAppSpaces.Transport.Sockets.Auth.Contract;

namespace XcoAppSpaces.Transport.Sockets
{
	/// <summary>
	/// Communication Service that send/receives messages to/from remote spaces via
	/// TCP sockets. The service is able to either use connections only one-way, meaning
	/// two spaces communicating with each other always use two channels (one for sending
	/// and one for receiving), or two-way, where a single connection between to spaces
	/// is used to transfer messages in both ways.
	/// </summary>
	public class XcoTCPTransportService : IXcoTransportService
	{
		/// <summary>
		/// Event that is raised when a message is received.
		/// </summary>
		public event MessageReceivedDelegate OnMessageReceived;
		
		private string hostAddress;
		private readonly string ipAddress = null;
		private int port = 0;
		private String hostname = null;
        bool running = false;
		
		private readonly IXcoLogger log = null;
		private TCPServer server = null;
        private readonly Dictionary<string, TCPClient> activeSenders = new Dictionary<string, TCPClient>();
		private object senderCreationLockObj = new object();

		internal static int timeoutCheckInterval = 30000; //check for connection timeouts every 30 seconds
		private TimeSpan connectionLifetime = TimeSpan.FromSeconds(60); //default is 60 seconds connection timeout
		private Timer timeoutCheckTimer; //the timer that triggers the timeout checks

		/// <summary>
		/// Security settings for authentication of incoming connections.
		/// </summary>
		public SecuritySettings SecuritySettings { get; private set; }

		/// <summary>
		/// Creates a new XcoTCPTransportService.
		/// </summary>
		public XcoTCPTransportService()
		{
			this.log = XcoLoggerFactory.CreateInstance(this.GetType());
			this.SecuritySettings = new SecuritySettings { AuthenticationKey = null }; //by default security is switched off
		}

		/// <summary>
		/// Creates a new XcoTCPTransportService.
		/// </summary>
		/// <param name="port">The port where the service should be running.</param>
		public XcoTCPTransportService(int port)
			: this()
		{
			this.port = port;
		}

		/// <summary>
		/// Creates a new XcoTCPTransportService.
		/// </summary>
		/// <param name="ipAddress">The local ip address where the service should be running.</param>
		/// <param name="port">The port where the service should be running.</param>
		public XcoTCPTransportService(string ipAddress, int port)
			: this()
		{
			this.ipAddress = ipAddress;
			this.port = port;
		}

		/// <summary>
		/// The port on which the service is running.
		/// </summary>
		public int Port
		{
			get { return port; }
			set
			{
				if (!running)
					port = value;
				else
					throw new XcoCommunicationException("Port cannot be set while the communication service is running.");
			}
		}

		/// <summary>
		/// The Address of the server.
		/// </summary>
		public string LocalAddress
		{
			get { return hostAddress; }
		}

		/// <summary>
		/// The time until a connect is closed when it has not been used (meaning not messages have been sent or received).
		/// </summary>
		public TimeSpan ConnectionLifetime
		{
			get { return connectionLifetime; }
			set { connectionLifetime = value; }
		}

		/// <summary>
		/// Returns a list of addresses to which there are currently active tcp connections.
		/// </summary>
		public IEnumerable<string> ActiveConnections
		{
			get
			{
				if (server == null)
					return null;
				IEnumerable<string> sendingConnections;
				lock (activeSenders)
				{
					sendingConnections = activeSenders.Keys.ToList();
				}
				return server.ActiveConnections.Union(sendingConnections);
			}
		}

		/// <summary>
		/// Initializes the transport service.
		/// </summary>
		/// <param name="serviceRegistry">Service registry with other services of the space instance.</param>
		public void Initialize(XcoAppSpaces.Contracts.Service.IXcoServiceRegistry serviceRegistry)
		{
			//nothing to do
		}

		/// <summary>
		/// Starts the communication service.
		/// </summary>
		public void Start()
		{
			if (server == null)
			{
				if (port == 0)
					log.Info("Starting XcoTCPTransportService on random port");
				else
					log.Info("Starting XcoTCPTransportService on port " + port);
				
				hostname = Config.Hostname;
				if (ipAddress != null)
					hostname = ipAddress;
				 
				//start server
				server = new TCPServer(ref port, ipAddress, this, log, SecuritySettings);
				running = true;

				hostAddress = hostname + ":" + port;

				//start timeout check timer
				timeoutCheckTimer = new Timer(_ => CheckConnectionTimeouts(), null, timeoutCheckInterval, timeoutCheckInterval);
			}
		}

		/// <summary>
		/// Start priority of the service.
		/// </summary>
		public int StartPriority
		{
			get { return 5; }
		}

		/// <summary>
		/// Stops the service and closes all open connections.
		/// </summary>
		public void Stop()
		{
			if (running)
			{
				running = false;
				timeoutCheckTimer.Dispose();
				ReleaseConnections();
				server.Stop();
			}
		}
		
		/// <summary>
		/// Releases all currently active connections.
		/// </summary>
		public void ReleaseConnections()
		{
			lock (activeSenders)
			{
				foreach (var client in activeSenders.Values)
				{
					client.Close();
				}
				activeSenders.Clear();
			}
		}

		/// <summary>
		/// Sends a message to a remote space.
		/// </summary>
		/// <param name="msg">The messages that should be sent.</param>
		/// <param name="remoteAddress">The address where the message should be sent to.</param>
		public void Send(XcoMessage msg, string remoteAddress)
		{
			if (!running)
				msg.RaiseTransferFailedEvent(new XcoCommunicationException("Server for remote communication has not been started!"));
			bool newConnection = true;
			TCPClient sender = null;
			try
			{
				//find remote sender or create new one, if no sender to this remote address exists
				sender = GetOrCreateSender(remoteAddress, out newConnection);
				log.Debug("Sending Message to " + remoteAddress);
				sender.Send(msg);
				if (newConnection)
					sender.Initialize();
			}
			catch (Exception) //ex)
			{
				//retry opening a new connection if the present connection is not new
				if (!newConnection)
				{
					try
					{
						log.Warn("Error sending Message to Address: " + remoteAddress + " using the currently open connection. Trying to open new connection.");
						sender = ReCreateSender(remoteAddress, sender, out newConnection);
						sender.Send(msg);
						if (newConnection)
							sender.Initialize();
					}
					catch (Exception ex2)
					{
						log.Error("Error sending Message to Address: " + remoteAddress, ex2);
						if (!newConnection)
							msg.RaiseTransferFailedEvent(new XcoCommunicationException("Error sending Message to Address \"" + remoteAddress + "\" : " + ex2.Message, ex2));
					}
				}
				//else
				//{
				//	log.Error("Error sending Message to Address: " + remoteAddress, ex);
				//	msg.RaiseTransferFailedEvent(new XcoCommunicationException("Error sending Message to Address \"" + remoteAddress + "\" : " + ex.Message, ex));
				//}
			}
		}

		private TCPClient GetSender(string remoteAddress)
		{
			lock (activeSenders)
			{
				TCPClient sender;
				return activeSenders.TryGetValue(remoteAddress, out sender) ? sender : null;
			}
		}

		private TCPClient CreateSender(string remoteAddress)
		{
			log.Debug("Opening new connection for sending messages to " + remoteAddress);
			var sender = new TCPClient(server, remoteAddress, this.LocalAddress, SecuritySettings, log);

			lock (activeSenders)
			{
				activeSenders.Add(remoteAddress, sender);
			}
			return sender;
		}

		private TCPClient GetOrCreateSender(string remoteAddress, out bool newConnection)
		{
			newConnection = false;
			var sender = GetSender(remoteAddress);
			if (sender == null)
			{
				lock (senderCreationLockObj)
				{
					sender = GetSender(remoteAddress);
					if (sender == null)
					{
						newConnection = true;
						sender = CreateSender(remoteAddress);
					}
				}
			}
			return sender;
		}

		private TCPClient ReCreateSender(string remoteAddress, TCPClient oldSender, out bool newConnection)
		{
			TCPClient sender;
			lock (senderCreationLockObj)
			{
				if (oldSender != null)
					oldSender.Close();
				lock (activeSenders)
				{
					TCPClient currentSender;
					if (activeSenders.TryGetValue(remoteAddress, out currentSender)) //it could happen that the sender has been removed by now (e.g. connection has been closed)
					{
						if (oldSender != null && currentSender != oldSender)
						{
							newConnection = false;
							sender = currentSender; // a new client has already been created, so recreation is not necessary
						}
						else
						{
							newConnection = true;
							activeSenders.Remove(remoteAddress);
							sender = CreateSender(remoteAddress);
						}
					}
					else
					{
						newConnection = true;
						sender = CreateSender(remoteAddress);
					}
				}
			}
			return sender;
		}

		/// <summary>
		/// Checks if any TCPClients have run into a timeout (not sent any message for a 
		/// defined amount of time), and removes these clients and closes their connections.
		/// </summary>
		private void CheckConnectionTimeouts()
		{
			DateTime checkDate = DateTime.Now.Subtract(connectionLifetime);

			//keep access time to the remoteSenders Dictionary as short as possible
			var timedOut = new List<TCPClient>();
			var timedOutKeys = new List<string>();
			lock (activeSenders)
			{
				foreach (var address in activeSenders.Keys)
				{
					var sender = activeSenders[address];
					if (sender.LastSendTime <= checkDate)
					{
						timedOut.Add(sender);
						timedOutKeys.Add(address);
						log.Debug("Sender timed out for remote space: " + address);
					}
				}
				//remove RemoteSenders from the list
				foreach (var key in timedOutKeys)
				{
					activeSenders.Remove(key);
				}
			}
			//disconnect senders that have timed out
			foreach (var sender in timedOut)
			{
				sender.Close();
			}
		}

		//private static int CreateRandomPort()
		//{
		//	return 8000 + (new Random()).Next(1000);
		//}

		internal void RaiseMessageReceivedEvent(XcoMessage msg, string remoteAddress)
		{
			OnMessageReceived(msg, remoteAddress, this);
		}

        #region IDisposable Members

		/// <summary>
		/// Stops the service and closes all open connections.
		/// </summary>
		public void Dispose()
        {
            this.Stop();
        }

        #endregion
	}
}
